Micron Document
🎖️GitЯра🎖️


Displaying Raw • Download

core/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/NodeInfoDao.kt 41414f832f938b655c97bc07509bc140ea16e2a3 (41414f83) Text, 27.81 KB

T8b949e/*
* Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
Tff7b72package T7ee787org.meshtastic.core.database.dao

Tff7b72import T7ee787androidx.room3.Dao
Tff7b72import T7ee787androidx.room3.MapColumn
Tff7b72import T7ee787androidx.room3.Query
Tff7b72import T7ee787androidx.room3.Transaction
Tff7b72import T7ee787androidx.room3.Upsert
Tff7b72import T7ee787kotlinx.coroutines.flow.Flow
Tff7b72import T7ee787okio.ByteString
Tff7b72import T7ee787org.meshtastic.core.common.util.crc32
Tff7b72import T7ee787org.meshtastic.core.database.entity.MetadataEntity
Tff7b72import T7ee787org.meshtastic.core.database.entity.MyNodeEntity
Tff7b72import T7ee787org.meshtastic.core.database.entity.NodeEntity
Tff7b72import T7ee787org.meshtastic.core.database.entity.NodeWithRelations
Tff7b72import T7ee787org.meshtastic.core.database.validDeviceIdOrNull
Tff7b72import T7ee787org.meshtastic.core.model.NodeAddress
Tff7b72import T7ee787org.meshtastic.core.model.mergePowerChannelLabel
Tff7b72import T7ee787org.meshtastic.proto.HardwareModel

Tf0883e@SuppressTb4b4b4(Ta5d6ff"Ta5d6ffTooManyFunctionsTa5d6ff"Tb4b4b4)
Tf0883e@Dao
Tff7b72interface T56d364NodeInfoDao Tb4b4b4{

Tff7b72companion Tff7b72object Tb4b4b4{
Tff7b72const Tff7b72val Te6edf3KEY_SIZE Tff7b72= T79c0ff3T79c0ff2

T8b949e/** SQLite has a limit of ~999 bind parameters per query. */
Tff7b72const Tff7b72val Te6edf3MAX_BIND_PARAMS Tff7b72= T79c0ff9T79c0ff9T79c0ff9
Tb4b4b4}

T8b949e/**
* Verifies a [NodeEntity] before an upsert operation. It handles populating the publicKey for lazy migration,
* checks for public key conflicts with new nodes, and manages updates to existing nodes, particularly in cases of
* public key mismatches to prevent potential impersonation or data corruption.
*
* @param incomingNode The node entity to be verified.
* @return A [NodeEntity] that is safe to upsert, or null if the upsert should be aborted (e.g., due to an
* impersonation attempt, though this logic is currently commented out).
*/
Tff7b72private Tff7b72suspend Tff7b72fun Td2a8ffgetVerifiedNodeForUpsertTb4b4b4(Te6edf3incomingNodeTb4b4b4: Te6edf3NodeEntityTb4b4b4)Tb4b4b4: Te6edf3NodeEntity Tb4b4b4{
T8b949e// Populate the NodeEntity.publicKey field from the User.publicKey for consistency
T8b949e// and to support lazy migration.
Te6edf3incomingNodeTb4b4b4.Te6edf3publicKey Tff7b72= Te6edf3incomingNodeTb4b4b4.Te6edf3userTb4b4b4.Te6edf3public_key

T8b949e// Populate denormalized name columns from the User protobuf for search functionality
T8b949e// Only populate if the user is not a placeholder (hwModel != UNSET); otherwise keep them null
Tff7b72if Tb4b4b4(Te6edf3incomingNodeTb4b4b4.Te6edf3userTb4b4b4.Te6edf3hw_model Tff7b72!Tff7b72= Te6edf3HardwareModelTb4b4b4.Te6edf3UNSETTb4b4b4) Tb4b4b4{
Te6edf3incomingNodeTb4b4b4.Te6edf3longName Tff7b72= Te6edf3incomingNodeTb4b4b4.Te6edf3userTb4b4b4.Te6edf3long_name
Te6edf3incomingNodeTb4b4b4.Te6edf3shortName Tff7b72= Te6edf3incomingNodeTb4b4b4.Te6edf3userTb4b4b4.Te6edf3short_name
Tb4b4b4} Tff7b72else Tb4b4b4{
Te6edf3incomingNodeTb4b4b4.Te6edf3longName Tff7b72= Tff7b72null
Te6edf3incomingNodeTb4b4b4.Te6edf3shortName Tff7b72= Tff7b72null
Tb4b4b4}

Tff7b72val Te6edf3existingNodeEntity Tff7b72= Te6edf3getNodeByNumTb4b4b4(Te6edf3incomingNodeTb4b4b4.Te6edf3numTb4b4b4)Tff7b72?.Te6edf3node

Tff7b72return Tff7b72if Tb4b4b4(Te6edf3existingNodeEntity Tff7b72=Tff7b72= Tff7b72nullTb4b4b4) Tb4b4b4{
Te6edf3handleNewNodeUpsertValidationTb4b4b4(Te6edf3incomingNodeTb4b4b4)
Tb4b4b4} Tff7b72else Tb4b4b4{
Te6edf3handleExistingNodeUpsertValidationTb4b4b4(Te6edf3existingNodeEntityTb4b4b4, Te6edf3incomingNodeTb4b4b4)
Tb4b4b4}
Tb4b4b4}

T8b949e/**
* Validates a new node before it is inserted into the database. This is the mesh-time (single upsert) path: anyone
* can broadcast a NodeInfo, so a known key arriving under a different num is conservatively mapped back to the
* existing identity — even when the num is the canonical crc32-derived one. Renumber migration happens only in
* [installConfig], where the entries come from the connected device's own vetted node DB over the local link.
*/
Tff7b72private Tff7b72suspend Tff7b72fun Td2a8ffhandleNewNodeUpsertValidationTb4b4b4(Te6edf3newNodeTb4b4b4: Te6edf3NodeEntityTb4b4b4)Tb4b4b4: Te6edf3NodeEntity Tb4b4b4{
T8b949e// Check if the new node's public key (if present and not empty)
T8b949e// is already claimed by another existing node.
Tff7b72if Tb4b4b4(Tb4b4b4(Te6edf3newNodeTb4b4b4.Te6edf3publicKeyTff7b72?.Te6edf3size Tff7b72?: T79c0ff0Tb4b4b4) Tff7b72> T79c0ff0Tb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3nodeWithSamePK Tff7b72= Te6edf3findNodeByPublicKeyTb4b4b4(Te6edf3newNodeTb4b4b4.Te6edf3publicKeyTb4b4b4)
Tff7b72if Tb4b4b4(Te6edf3nodeWithSamePK Tff7b72!Tff7b72= Tff7b72null Tff7b72&Tff7b72& Te6edf3nodeWithSamePKTb4b4b4.Te6edf3num Tff7b72!Tff7b72= Te6edf3newNodeTb4b4b4.Te6edf3numTb4b4b4) Tb4b4b4{
T8b949e// This is a potential impersonation attempt.
Tff7b72return Te6edf3nodeWithSamePK
Tb4b4b4}
Tb4b4b4}
T8b949e// If no conflicting public key is found, or if the impersonation check is not active,
T8b949e// the new node is considered safe to add.
Tff7b72return Te6edf3newNode
Tb4b4b4}

T8b949e/**
* True when this node's num is the canonical pubkey-derived number introduced by firmware 2.8 (`my_node_num =
* crc32(public_key)`, see NodeDB::createNewIdentity). A canonical num proves the num/key pairing was not chosen
* independently of the key, so a same-key-new-num sighting is a renumber rather than a spoof. Only consulted for
* entries streamed by the connected device during a config install (trusted local link), never for mesh packets.
*/
Tff7b72private Tff7b72fun Te6edf3NodeEntityTb4b4b4.Td2a8ffhasCanonicalNumTb4b4b4(Tb4b4b4)Tb4b4b4: Tffa657Boolean Tb4b4b4{
Tff7b72val Te6edf3key Tff7b72= Te6edf3publicKey Tff7b72?: Te6edf3userTb4b4b4.Te6edf3public_key
Tff7b72return Te6edf3keyTb4b4b4.Te6edf3size Tff7b72=Tff7b72= Te6edf3KEY_SIZE Tff7b72&Tff7b72& Te6edf3key Tff7b72!Tff7b72= Te6edf3NodeEntityTb4b4b4.Te6edf3ERROR_BYTE_STRING Tff7b72&Tff7b72& Te6edf3keyTb4b4b4.Te6edf3crc32Tb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3toIntTb4b4b4(Tb4b4b4) Tff7b72=Tff7b72= Te6edf3num
Tb4b4b4}

T8b949e/**
* Moves a node's identity from the [from] row to the incoming [to] entity after a legitimate renumber (the local
* device reconciled by [migrateLocalNodeIdentity], or a mesh peer whose new num is its canonical pubkey-derived
* number). Carries the app-local state the mesh doesn't broadcast, drops the stale row, and re-keys the DM thread
* so the conversation follows the node. The caller upserts [to] afterwards.
*/
Tff7b72private Tff7b72suspend Tff7b72fun Td2a8ffmigrateNodeIdentityTb4b4b4(Te6edf3fromTb4b4b4: Te6edf3NodeEntityTb4b4b4, Te6edf3toTb4b4b4: Te6edf3NodeEntityTb4b4b4) Tb4b4b4{
Tff7b72if Tb4b4b4(Te6edf3toTb4b4b4.Te6edf3notesTb4b4b4.Te6edf3isBlankTb4b4b4(Tb4b4b4)Tb4b4b4) Te6edf3toTb4b4b4.Te6edf3notes Tff7b72= Te6edf3fromTb4b4b4.Te6edf3notes
Tff7b72if Tb4b4b4(Te6edf3toTb4b4b4.Te6edf3powerChannelLabelsTb4b4b4.Te6edf3isEmptyTb4b4b4(Tb4b4b4)Tb4b4b4) Te6edf3toTb4b4b4.Te6edf3powerChannelLabels Tff7b72= Te6edf3fromTb4b4b4.Te6edf3powerChannelLabels
Te6edf3toTb4b4b4.Te6edf3manuallyVerified Tff7b72= Te6edf3toTb4b4b4.Te6edf3manuallyVerified Tff7b72|Tff7b72| Te6edf3fromTb4b4b4.Te6edf3manuallyVerified
Te6edf3toTb4b4b4.Te6edf3isFavorite Tff7b72= Te6edf3toTb4b4b4.Te6edf3isFavorite Tff7b72|Tff7b72| Te6edf3fromTb4b4b4.Te6edf3isFavorite
Te6edf3toTb4b4b4.Te6edf3isIgnored Tff7b72= Te6edf3toTb4b4b4.Te6edf3isIgnored Tff7b72|Tff7b72| Te6edf3fromTb4b4b4.Te6edf3isIgnored
Te6edf3toTb4b4b4.Te6edf3isMuted Tff7b72= Te6edf3toTb4b4b4.Te6edf3isMuted Tff7b72|Tff7b72| Te6edf3fromTb4b4b4.Te6edf3isMuted
Te6edf3deleteNodeTb4b4b4(Te6edf3fromTb4b4b4.Te6edf3numTb4b4b4)
Te6edf3deleteMetadataTb4b4b4(Te6edf3fromTb4b4b4.Te6edf3numTb4b4b4)
Tff7b72val Te6edf3oldId Tff7b72= Te6edf3fromTb4b4b4.Te6edf3userTb4b4b4.Te6edf3idTb4b4b4.Te6edf3ifEmpty Tb4b4b4{ Te6edf3NodeAddressTb4b4b4.Te6edf3numToDefaultIdTb4b4b4(Te6edf3fromTb4b4b4.Te6edf3numTb4b4b4) Tb4b4b4}
Tff7b72val Te6edf3newId Tff7b72= Te6edf3toTb4b4b4.Te6edf3userTb4b4b4.Te6edf3idTb4b4b4.Te6edf3ifEmpty Tb4b4b4{ Te6edf3NodeAddressTb4b4b4.Te6edf3numToDefaultIdTb4b4b4(Te6edf3toTb4b4b4.Te6edf3numTb4b4b4) Tb4b4b4}
Tff7b72if Tb4b4b4(Te6edf3oldId Tff7b72!Tff7b72= Te6edf3newIdTb4b4b4) Tb4b4b4{
Te6edf3remapDmContactKeyTb4b4b4(Te6edf3oldIdTb4b4b4, Te6edf3newIdTb4b4b4)
Te6edf3remapDmContactSettingsTb4b4b4(Te6edf3oldIdTb4b4b4, Te6edf3newIdTb4b4b4)
Te6edf3deleteDmContactSettingsTb4b4b4(Te6edf3oldIdTb4b4b4)
Tb4b4b4}
Tb4b4b4}

T8b949e/**
* Resolves the public key for an existing node during an update.
*
* This function implements safety checks to prevent public key conflicts (PKC) and ensure robust handling of key
* updates.
*
* @param existingNode The current state of the node in the database.
* @param incomingNode The new node data being upserted.
* @return The resolved [ByteString] for the public key:
* - [NodeEntity.ERROR_BYTE_STRING]: If there is a mismatch between a valid existing key and a new incoming key.
* - `incomingNode.publicKey`: If the incoming key is new, matches the existing one, or if recovering from an error
* state.
* - `existingNode.publicKey`: If the incoming update has no key, or if the user is licensed but already has a valid
* key (prevents wiping).
* - [ByteString.EMPTY]: If the user is licensed and didn't previously have a key (or if key is explicitly cleared).
*/
Tff7b72private Tff7b72fun Td2a8ffresolvePublicKeyTb4b4b4(Te6edf3existingNodeTb4b4b4: Te6edf3NodeEntityTb4b4b4, Te6edf3incomingNodeTb4b4b4: Te6edf3NodeEntityTb4b4b4)Tb4b4b4: Te6edf3ByteString? Tb4b4b4{
Tff7b72val Te6edf3existingKey Tff7b72= Te6edf3existingNodeTb4b4b4.Te6edf3publicKey Tff7b72?: Te6edf3existingNodeTb4b4b4.Te6edf3userTb4b4b4.Te6edf3public_key
Tff7b72val Te6edf3incomingKey Tff7b72= Te6edf3incomingNodeTb4b4b4.Te6edf3publicKey

Tff7b72val Te6edf3incomingHasKey Tff7b72= Tb4b4b4(Te6edf3incomingKeyTff7b72?.Te6edf3size Tff7b72?: T79c0ff0Tb4b4b4) Tff7b72=Tff7b72= Te6edf3KEY_SIZE
Tff7b72val Te6edf3existingHasKey Tff7b72= Te6edf3existingKeyTb4b4b4.Te6edf3size Tff7b72=Tff7b72= Te6edf3KEY_SIZE Tff7b72&Tff7b72& Te6edf3existingKey Tff7b72!Tff7b72= Te6edf3NodeEntityTb4b4b4.Te6edf3ERROR_BYTE_STRING

Tff7b72return Tff7b72when Tb4b4b4{
Te6edf3incomingHasKey Tff7b72-Tff7b72> Tb4b4b4{
Tff7b72if Tb4b4b4(Te6edf3existingHasKey Tff7b72&Tff7b72& Te6edf3incomingKey Tff7b72!Tff7b72= Te6edf3existingKeyTb4b4b4) Tb4b4b4{
T8b949e// Actual mismatch between two non-empty keys
Te6edf3NodeEntityTb4b4b4.Te6edf3ERROR_BYTE_STRING
Tb4b4b4} Tff7b72else Tb4b4b4{
T8b949e// New key, same key, or recovery from Error state
Te6edf3incomingKey
Tb4b4b4}
Tb4b4b4}

Te6edf3existingHasKey Tff7b72-Tff7b72> Te6edf3existingKey

Te6edf3incomingNodeTb4b4b4.Te6edf3userTb4b4b4.Te6edf3is_licensed Tff7b72-Tff7b72> Te6edf3ByteStringTb4b4b4.Te6edf3EMPTY

Tff7b72else Tff7b72-Tff7b72> Te6edf3existingKey
Tb4b4b4}
Tb4b4b4}

T8b949e/**
* Handles the validation logic when upserting an existing node.
*
* It distinguishes between two scenarios:
* 1. **Preservation**: If the incoming update is a placeholder (unset HW model) with a default name, and the
* existing node has full user info, we preserve the existing identity (user, keys, names, verification) while
* updating telemetry and status fields from the incoming packet.
* 2. **Update**: If it's a normal update, we validate the public key using [resolvePublicKey] to prevent conflicts
* or accidental key wiping, and then update the node.
*
* [trustIncomingKey] skips the mismatch check and accepts a valid incoming key as-is. Set only for the local node
* during a config install: the connected device is authoritative for its own key over the local link, and an
* erase-and-reflash legitimately re-keys it (a mismatch there would otherwise poison the local node with
* [NodeEntity.ERROR_BYTE_STRING] and break PKI traffic until app data is cleared).
*/
Tf0883e@SuppressTb4b4b4(Ta5d6ff"Ta5d6ffCyclomaticComplexMethodTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffMagicNumberTa5d6ff"Tb4b4b4)
Tff7b72private Tff7b72fun Td2a8ffhandleExistingNodeUpsertValidationTb4b4b4(
Te6edf3existingNodeTb4b4b4: Te6edf3NodeEntityTb4b4b4,
Te6edf3incomingNodeTb4b4b4: Te6edf3NodeEntityTb4b4b4,
Te6edf3trustIncomingKeyTb4b4b4: Tffa657Boolean Tff7b72= Tff7b72falseTb4b4b4,
Tb4b4b4)Tb4b4b4: Te6edf3NodeEntity Tb4b4b4{
Tff7b72val Te6edf3resolvedNotes Tff7b72= Te6edf3incomingNodeTb4b4b4.Te6edf3notesTb4b4b4.Te6edf3ifBlank Tb4b4b4{ Te6edf3existingNodeTb4b4b4.Te6edf3notes Tb4b4b4}
Tff7b72val Te6edf3resolvedPowerChannelLabels Tff7b72= Te6edf3incomingNodeTb4b4b4.Te6edf3powerChannelLabelsTb4b4b4.Te6edf3ifEmpty Tb4b4b4{ Te6edf3existingNodeTb4b4b4.Te6edf3powerChannelLabels Tb4b4b4}

Tff7b72val Te6edf3isPlaceholder Tff7b72= Te6edf3incomingNodeTb4b4b4.Te6edf3userTb4b4b4.Te6edf3hw_model Tff7b72=Tff7b72= Te6edf3HardwareModelTb4b4b4.Te6edf3UNSET
Tff7b72val Te6edf3hasExistingUser Tff7b72= Te6edf3existingNodeTb4b4b4.Te6edf3userTb4b4b4.Te6edf3hw_model Tff7b72!Tff7b72= Te6edf3HardwareModelTb4b4b4.Te6edf3UNSET
Tff7b72val Te6edf3isDefaultName Tff7b72= Te6edf3incomingNodeTb4b4b4.Te6edf3userTb4b4b4.Te6edf3long_nameTb4b4b4.Te6edf3matchesTb4b4b4(Te6edf3RegexTb4b4b4(Ta5d6ff"Ta5d6ff^Meshtastic [0-9a-fA-F]{4}$Ta5d6ff"Tb4b4b4)Tb4b4b4)

Tff7b72if Tb4b4b4(Te6edf3hasExistingUser Tff7b72&Tff7b72& Te6edf3isPlaceholder Tff7b72&Tff7b72& Te6edf3isDefaultNameTb4b4b4) Tb4b4b4{
Tff7b72return Te6edf3incomingNodeTb4b4b4.Te6edf3copyTb4b4b4(
Te6edf3user Tff7b72= Te6edf3existingNodeTb4b4b4.Te6edf3userTb4b4b4,
Te6edf3publicKey Tff7b72= Te6edf3existingNodeTb4b4b4.Te6edf3publicKeyTb4b4b4,
Te6edf3longName Tff7b72= Te6edf3existingNodeTb4b4b4.Te6edf3longNameTb4b4b4,
Te6edf3shortName Tff7b72= Te6edf3existingNodeTb4b4b4.Te6edf3shortNameTb4b4b4,
Te6edf3manuallyVerified Tff7b72= Te6edf3existingNodeTb4b4b4.Te6edf3manuallyVerifiedTb4b4b4,
Te6edf3notes Tff7b72= Te6edf3resolvedNotesTb4b4b4,
Te6edf3powerChannelLabels Tff7b72= Te6edf3resolvedPowerChannelLabelsTb4b4b4,
Tb4b4b4)
Tb4b4b4}

Tff7b72val Te6edf3resolvedKey Tff7b72=
Tff7b72if Tb4b4b4(Te6edf3trustIncomingKey Tff7b72&Tff7b72& Tb4b4b4(Te6edf3incomingNodeTb4b4b4.Te6edf3publicKeyTff7b72?.Te6edf3size Tff7b72?: T79c0ff0Tb4b4b4) Tff7b72=Tff7b72= Te6edf3KEY_SIZETb4b4b4) Tb4b4b4{
Te6edf3incomingNodeTb4b4b4.Te6edf3publicKey
Tb4b4b4} Tff7b72else Tb4b4b4{
Te6edf3resolvePublicKeyTb4b4b4(Te6edf3existingNodeTb4b4b4, Te6edf3incomingNodeTb4b4b4)
Tb4b4b4}

Tff7b72return Te6edf3incomingNodeTb4b4b4.Te6edf3copyTb4b4b4(
Te6edf3user Tff7b72= Te6edf3incomingNodeTb4b4b4.Te6edf3userTb4b4b4.Te6edf3copyTb4b4b4(Te6edf3public_key Tff7b72= Te6edf3resolvedKey Tff7b72?: Te6edf3ByteStringTb4b4b4.Te6edf3EMPTYTb4b4b4)Tb4b4b4,
Te6edf3publicKey Tff7b72= Te6edf3resolvedKeyTb4b4b4,
Te6edf3notes Tff7b72= Te6edf3resolvedNotesTb4b4b4,
Te6edf3powerChannelLabels Tff7b72= Te6edf3resolvedPowerChannelLabelsTb4b4b4,
Tb4b4b4)
Tb4b4b4}

Tf0883e@QueryTb4b4b4(Ta5d6ff"Ta5d6ffSELECT * FROM my_nodeTa5d6ff"Tb4b4b4)
Tff7b72fun Td2a8ffgetMyNodeInfoTb4b4b4(Tb4b4b4)Tb4b4b4: Te6edf3FlowTff7b72<Te6edf3MyNodeEntity?Tff7b72>

Tf0883e@Upsert Tff7b72suspend Tff7b72fun Td2a8ffsetMyNodeInfoTb4b4b4(Te6edf3myInfoTb4b4b4: Te6edf3MyNodeEntityTb4b4b4)

Tf0883e@QueryTb4b4b4(Ta5d6ff"Ta5d6ffDELETE FROM my_nodeTa5d6ff"Tb4b4b4)
Tff7b72suspend Tff7b72fun Td2a8ffclearMyNodeInfoTb4b4b4(Tb4b4b4)

Tf0883e@QueryTb4b4b4(
Ta5d6ff"""
Ta5d6ff SELECT * FROM nodes
ORDER BY CASE
WHEN num = (SELECT myNodeNum FROM my_node LIMIT 1) THEN 0
ELSE 1
END,
last_heard DESC
Ta5d6ff"""Tb4b4b4,
Tb4b4b4)
Tf0883e@Transaction
Tff7b72fun Td2a8ffnodeDBbyNumTb4b4b4(Tb4b4b4)Tb4b4b4: Te6edf3FlowTff7b72<
Te6edf3MapTff7b72<
Tf0883e@MapColumnTb4b4b4(Te6edf3columnName Tff7b72= Ta5d6ff"Ta5d6ffnumTa5d6ff"Tb4b4b4)
Tffa657IntTb4b4b4,
Te6edf3NodeWithRelationsTb4b4b4,
Tff7b72>Tb4b4b4,
Tff7b72>

T8b949e/**
* One-shot snapshot of every node row in the CURRENTLY SELECTED database (not the process-wide stateIn cache). Used
* by session-safe cache loads (see NodeManagerImpl.loadCachedNodeDB) so a transport switch can't deliver a stale
* pre-switch map.
*/
Tf0883e@QueryTb4b4b4(
Ta5d6ff"""
Ta5d6ff SELECT * FROM nodes
ORDER BY CASE
WHEN num = (SELECT myNodeNum FROM my_node LIMIT 1) THEN 0
ELSE 1
END,
last_heard DESC
Ta5d6ff"""Tb4b4b4,
Tb4b4b4)
Tf0883e@Transaction
Tff7b72suspend Tff7b72fun Td2a8ffnodeDBbyNumSnapshotTb4b4b4(Tb4b4b4)Tb4b4b4: Te6edf3MapTff7b72<
Tf0883e@MapColumnTb4b4b4(Te6edf3columnName Tff7b72= Ta5d6ff"Ta5d6ffnumTa5d6ff"Tb4b4b4)
Tffa657IntTb4b4b4,
Te6edf3NodeWithRelationsTb4b4b4,
Tff7b72>

Tf0883e@QueryTb4b4b4(
Ta5d6ff"""
Ta5d6ff WITH OurNode AS (
SELECT latitude, longitude
FROM nodes
WHERE num = (SELECT myNodeNum FROM my_node LIMIT 1)
)
SELECT * FROM nodes
WHERE (:includeUnknown = 1 OR short_name IS NOT NULL)
AND (:filter = ''
OR (long_name LIKE '%' || :filter || '%'
OR short_name LIKE '%' || :filter || '%'
OR printf('!%08x', CASE WHEN num < 0 THEN num + 4294967296 ELSE num END) LIKE '%' || :filter || '%'
OR CAST(CASE WHEN num < 0 THEN num + 4294967296 ELSE num END AS TEXT) LIKE '%' || :filter || '%'))
AND (:lastHeardMin = -1 OR last_heard >= :lastHeardMin)
AND (:hopsAwayMax = -1 OR (hops_away <= :hopsAwayMax AND hops_away >= 0) OR num = (SELECT myNodeNum FROM my_node LIMIT 1))
ORDER BY CASE
WHEN num = (SELECT myNodeNum FROM my_node LIMIT 1) THEN 0
ELSE 1
END,
CASE
WHEN :sort = 'last_heard' THEN last_heard * -1
WHEN :sort = 'alpha' THEN UPPER(long_name)
WHEN :sort = 'distance' THEN
CASE
WHEN latitude IS NULL OR longitude IS NULL OR
(latitude = 0.0 AND longitude = 0.0) THEN 999999999
ELSE
(latitude - (SELECT latitude FROM OurNode)) *
(latitude - (SELECT latitude FROM OurNode)) +
(longitude - (SELECT longitude FROM OurNode)) *
(longitude - (SELECT longitude FROM OurNode))
END
WHEN :sort = 'hops_away' THEN
CASE
WHEN hops_away = -1 THEN 999999999
ELSE hops_away
END
WHEN :sort = 'channel' THEN channel
WHEN :sort = 'via_mqtt' THEN via_mqtt
WHEN :sort = 'via_favorite' THEN is_favorite * -1
ELSE 0
END ASC,
last_heard DESC
Ta5d6ff"""Tb4b4b4,
Tb4b4b4)
Tf0883e@Transaction
Tff7b72fun Td2a8ffgetNodesTb4b4b4(
Te6edf3sortTb4b4b4: Tffa657StringTb4b4b4,
Te6edf3filterTb4b4b4: Tffa657StringTb4b4b4,
Te6edf3includeUnknownTb4b4b4: Tffa657BooleanTb4b4b4,
Te6edf3hopsAwayMaxTb4b4b4: Tffa657IntTb4b4b4,
Te6edf3lastHeardMinTb4b4b4: Tffa657IntTb4b4b4,
Tb4b4b4)Tb4b4b4: Te6edf3FlowTff7b72<Te6edf3ListTff7b72<Te6edf3NodeWithRelationsTff7b72>Tff7b72>

Tf0883e@Transaction
Tff7b72suspend Tff7b72fun Td2a8ffclearNodeInfoTb4b4b4(Te6edf3preserveFavoritesTb4b4b4: Tffa657BooleanTb4b4b4) Tb4b4b4{
Tff7b72if Tb4b4b4(Te6edf3preserveFavoritesTb4b4b4) Tb4b4b4{
Te6edf3deleteNonFavoriteNodesTb4b4b4(Tb4b4b4)
Tb4b4b4} Tff7b72else Tb4b4b4{
Te6edf3deleteAllNodesTb4b4b4(Tb4b4b4)
Tb4b4b4}
Tb4b4b4}

Tf0883e@QueryTb4b4b4(Ta5d6ff"Ta5d6ffDELETE FROM nodes WHERE is_favorite = 0Ta5d6ff"Tb4b4b4)
Tff7b72suspend Tff7b72fun Td2a8ffdeleteNonFavoriteNodesTb4b4b4(Tb4b4b4)

Tf0883e@QueryTb4b4b4(Ta5d6ff"Ta5d6ffDELETE FROM nodesTa5d6ff"Tb4b4b4)
Tff7b72suspend Tff7b72fun Td2a8ffdeleteAllNodesTb4b4b4(Tb4b4b4)

Tf0883e@QueryTb4b4b4(Ta5d6ff"Ta5d6ffDELETE FROM nodes WHERE num=:numTa5d6ff"Tb4b4b4)
Tff7b72suspend Tff7b72fun Td2a8ffdeleteNodeTb4b4b4(Te6edf3numTb4b4b4: Tffa657IntTb4b4b4)

Tf0883e@QueryTb4b4b4(Ta5d6ff"Ta5d6ffDELETE FROM nodes WHERE num IN (:nodeNums)Ta5d6ff"Tb4b4b4)
Tff7b72suspend Tff7b72fun Td2a8ffdeleteNodesTb4b4b4(Te6edf3nodeNumsTb4b4b4: Te6edf3ListTff7b72<Tffa657IntTff7b72>Tb4b4b4)

Tf0883e@QueryTb4b4b4(Ta5d6ff"Ta5d6ffSELECT * FROM nodes WHERE last_heard < :lastHeardTa5d6ff"Tb4b4b4)
Tff7b72suspend Tff7b72fun Td2a8ffgetNodesOlderThanTb4b4b4(Te6edf3lastHeardTb4b4b4: Tffa657IntTb4b4b4)Tb4b4b4: Te6edf3ListTff7b72<Te6edf3NodeEntityTff7b72>

Tf0883e@QueryTb4b4b4(Ta5d6ff"Ta5d6ffSELECT * FROM nodes WHERE short_name IS NULLTa5d6ff"Tb4b4b4)
Tff7b72suspend Tff7b72fun Td2a8ffgetUnknownNodesTb4b4b4(Tb4b4b4)Tb4b4b4: Te6edf3ListTff7b72<Te6edf3NodeEntityTff7b72>

Tf0883e@Upsert Tff7b72suspend Tff7b72fun Td2a8ffupsertTb4b4b4(Te6edf3metaTb4b4b4: Te6edf3MetadataEntityTb4b4b4)

Tf0883e@QueryTb4b4b4(Ta5d6ff"Ta5d6ffDELETE FROM metadata WHERE num=:numTa5d6ff"Tb4b4b4)
Tff7b72suspend Tff7b72fun Td2a8ffdeleteMetadataTb4b4b4(Te6edf3numTb4b4b4: Tffa657IntTb4b4b4)

Tf0883e@QueryTb4b4b4(Ta5d6ff"Ta5d6ffDELETE FROM metadata WHERE num IN (:nodeNums)Ta5d6ff"Tb4b4b4)
Tff7b72suspend Tff7b72fun Td2a8ffdeleteMetadataForNodesTb4b4b4(Te6edf3nodeNumsTb4b4b4: Te6edf3ListTff7b72<Tffa657IntTff7b72>Tb4b4b4)

T8b949e/** Atomically deletes one node and its separately stored device metadata. */
Tf0883e@Transaction
Tff7b72suspend Tff7b72fun Td2a8ffdeleteNodeAndMetadataTb4b4b4(Te6edf3numTb4b4b4: Tffa657IntTb4b4b4) Tb4b4b4{
Te6edf3deleteNodeTb4b4b4(Te6edf3numTb4b4b4)
Te6edf3deleteMetadataTb4b4b4(Te6edf3numTb4b4b4)
Tb4b4b4}

T8b949e/**
* Atomically deletes nodes and their metadata, chunking both `IN` queries below SQLite's bind-parameter limit.
* Every chunk participates in the same transaction, so cancellation or a query failure rolls back the whole batch.
*/
Tf0883e@Transaction
Tff7b72suspend Tff7b72fun Td2a8ffdeleteNodesAndMetadataTb4b4b4(Te6edf3nodeNumsTb4b4b4: Te6edf3ListTff7b72<Tffa657IntTff7b72>Tb4b4b4) Tb4b4b4{
Tff7b72for Tb4b4b4(Te6edf3chunk Tff7b72in Te6edf3nodeNumsTb4b4b4.Te6edf3chunkedTb4b4b4(Te6edf3MAX_BIND_PARAMSTb4b4b4)Tb4b4b4) Tb4b4b4{
Te6edf3deleteNodesTb4b4b4(Te6edf3chunkTb4b4b4)
Te6edf3deleteMetadataForNodesTb4b4b4(Te6edf3chunkTb4b4b4)
Tb4b4b4}
Tb4b4b4}

T8b949e/** Snapshot used by DatabaseMerger to carry per-node DeviceMetadata across transports (newest timestamp wins). */
Tf0883e@QueryTb4b4b4(Ta5d6ff"Ta5d6ffSELECT * FROM metadataTa5d6ff"Tb4b4b4)
Tff7b72suspend Tff7b72fun Td2a8ffgetAllMetadataSnapshotTb4b4b4(Tb4b4b4)Tb4b4b4: Te6edf3ListTff7b72<Te6edf3MetadataEntityTff7b72>

Tf0883e@QueryTb4b4b4(Ta5d6ff"Ta5d6ffSELECT * FROM nodes WHERE num=:numTa5d6ff"Tb4b4b4)
Tf0883e@Transaction
Tff7b72suspend Tff7b72fun Td2a8ffgetNodeByNumTb4b4b4(Te6edf3numTb4b4b4: Tffa657IntTb4b4b4)Tb4b4b4: Te6edf3NodeWithRelations?

Tf0883e@QueryTb4b4b4(Ta5d6ff"Ta5d6ffSELECT * FROM nodes WHERE num IN (:nodeNums)Ta5d6ff"Tb4b4b4)
Tff7b72suspend Tff7b72fun Td2a8ffgetNodeEntitiesByNumsTb4b4b4(Te6edf3nodeNumsTb4b4b4: Te6edf3ListTff7b72<Tffa657IntTff7b72>Tb4b4b4)Tb4b4b4: Te6edf3ListTff7b72<Te6edf3NodeEntityTff7b72>

Tf0883e@QueryTb4b4b4(Ta5d6ff"Ta5d6ffSELECT * FROM nodes WHERE public_key = :publicKey LIMIT 1Ta5d6ff"Tb4b4b4)
Tff7b72suspend Tff7b72fun Td2a8fffindNodeByPublicKeyTb4b4b4(Te6edf3publicKeyTb4b4b4: Te6edf3ByteString?Tb4b4b4)Tb4b4b4: Te6edf3NodeEntity?

Tf0883e@QueryTb4b4b4(Ta5d6ff"Ta5d6ffSELECT * FROM nodes WHERE public_key IN (:publicKeys)Ta5d6ff"Tb4b4b4)
Tff7b72suspend Tff7b72fun Td2a8fffindNodesByPublicKeysTb4b4b4(Te6edf3publicKeysTb4b4b4: Te6edf3ListTff7b72<Te6edf3ByteStringTff7b72>Tb4b4b4)Tb4b4b4: Te6edf3ListTff7b72<Te6edf3NodeEntityTff7b72>

Tf0883e@Upsert Tff7b72suspend Tff7b72fun Td2a8ffdoUpsertTb4b4b4(Te6edf3nodeTb4b4b4: Te6edf3NodeEntityTb4b4b4)

Tf0883e@Transaction
Tff7b72suspend Tff7b72fun Td2a8ffupsertTb4b4b4(Te6edf3nodeTb4b4b4: Te6edf3NodeEntityTb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3verifiedNode Tff7b72= Te6edf3getVerifiedNodeForUpsertTb4b4b4(Te6edf3nodeTb4b4b4)
Te6edf3doUpsertTb4b4b4(Te6edf3verifiedNodeTb4b4b4)
Tb4b4b4}

Tf0883e@Upsert Tff7b72suspend Tff7b72fun Td2a8ffputAllTb4b4b4(Te6edf3nodesTb4b4b4: Te6edf3ListTff7b72<Te6edf3NodeEntityTff7b72>Tb4b4b4)

Tf0883e@QueryTb4b4b4(Ta5d6ff"Ta5d6ffUPDATE nodes SET notes = :notes WHERE num = :numTa5d6ff"Tb4b4b4)
Tff7b72suspend Tff7b72fun Td2a8ffsetNodeNotesTb4b4b4(Te6edf3numTb4b4b4: Tffa657IntTb4b4b4, Te6edf3notesTb4b4b4: Tffa657StringTb4b4b4)

Tf0883e@QueryTb4b4b4(Ta5d6ff"Ta5d6ffUPDATE nodes SET power_channel_labels = :labels WHERE num = :numTa5d6ff"Tb4b4b4)
Tff7b72suspend Tff7b72fun Td2a8ffsetPowerChannelLabelsTb4b4b4(Te6edf3numTb4b4b4: Tffa657IntTb4b4b4, Te6edf3labelsTb4b4b4: Te6edf3ListTff7b72<Tffa657StringTff7b72>Tb4b4b4)

T8b949e/**
* Sets a single power-channel [label] (0-based [channelIndex]) atomically: the read-modify-write runs inside a
* transaction that reads the current labels from the DB, so concurrent edits to different channels can't clobber
* each other via a stale read. Earlier channels keep their slot (blank padding); trailing blanks are dropped.
*
* Reads via [getNodeByNum] rather than a scalar `SELECT power_channel_labels`: Room reads a `List<String>` query
* result as one String per row (returning the raw JSON text), not as the column's converted `List<String>`.
*/
Tf0883e@Transaction
Tff7b72suspend Tff7b72fun Td2a8ffupdatePowerChannelLabelTb4b4b4(Te6edf3numTb4b4b4: Tffa657IntTb4b4b4, Te6edf3channelIndexTb4b4b4: Tffa657IntTb4b4b4, Te6edf3labelTb4b4b4: Tffa657StringTb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3existing Tff7b72= Te6edf3getNodeByNumTb4b4b4(Te6edf3numTb4b4b4)Tff7b72?.Te6edf3nodeTff7b72?.Te6edf3powerChannelLabels Tff7b72?: Te6edf3emptyListTb4b4b4(Tb4b4b4)
Te6edf3setPowerChannelLabelsTb4b4b4(Te6edf3numTb4b4b4, Te6edf3mergePowerChannelLabelTb4b4b4(Te6edf3existingTb4b4b4, Te6edf3channelIndexTb4b4b4, Te6edf3labelTb4b4b4)Tb4b4b4)
Tb4b4b4}

T8b949e/**
* Batch version of [getVerifiedNodeForUpsert]. Pre-fetches all existing nodes and public-key conflicts in two
* queries instead of N individual queries, then processes each node in memory.
*
* [selfNum] is the connected device's node number during a config install; its key updates are trusted (see
* [handleExistingNodeUpsertValidation]) and a key conflict against it always migrates. Node numbers whose rows were
* migrated away are appended to [removedNums].
*/
Tf0883e@SuppressTb4b4b4(Ta5d6ff"Ta5d6ffNestedBlockDepthTa5d6ff"Tb4b4b4)
Tff7b72private Tff7b72suspend Tff7b72fun Td2a8ffgetVerifiedNodesForUpsertTb4b4b4(
Te6edf3incomingNodesTb4b4b4: Te6edf3ListTff7b72<Te6edf3NodeEntityTff7b72>Tb4b4b4,
Te6edf3selfNumTb4b4b4: Tffa657Int? Tff7b72= Tff7b72nullTb4b4b4,
Te6edf3removedNumsTb4b4b4: Te6edf3MutableListTff7b72<Tffa657IntTff7b72> Tff7b72= Te6edf3mutableListOfTb4b4b4(Tb4b4b4)Tb4b4b4,
Tb4b4b4)Tb4b4b4: Te6edf3ListTff7b72<Te6edf3NodeEntityTff7b72> Tb4b4b4{
T8b949e// Prepare all incoming nodes (populate denormalized fields)
Te6edf3incomingNodesTb4b4b4.Te6edf3forEach Tb4b4b4{ Te6edf3node Tff7b72-Tff7b72>
Te6edf3nodeTb4b4b4.Te6edf3publicKey Tff7b72= Te6edf3nodeTb4b4b4.Te6edf3userTb4b4b4.Te6edf3public_key
Tff7b72if Tb4b4b4(Te6edf3nodeTb4b4b4.Te6edf3userTb4b4b4.Te6edf3hw_model Tff7b72!Tff7b72= Te6edf3HardwareModelTb4b4b4.Te6edf3UNSETTb4b4b4) Tb4b4b4{
Te6edf3nodeTb4b4b4.Te6edf3longName Tff7b72= Te6edf3nodeTb4b4b4.Te6edf3userTb4b4b4.Te6edf3long_name
Te6edf3nodeTb4b4b4.Te6edf3shortName Tff7b72= Te6edf3nodeTb4b4b4.Te6edf3userTb4b4b4.Te6edf3short_name
Tb4b4b4} Tff7b72else Tb4b4b4{
Te6edf3nodeTb4b4b4.Te6edf3longName Tff7b72= Tff7b72null
Te6edf3nodeTb4b4b4.Te6edf3shortName Tff7b72= Tff7b72null
Tb4b4b4}
Tb4b4b4}

T8b949e// Batch fetch all existing nodes by num (chunked for SQLite bind-param limit)
Tff7b72val Te6edf3existingNodesMap Tff7b72=
Te6edf3incomingNodes
Tb4b4b4.Te6edf3map Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3num Tb4b4b4}
Tb4b4b4.Te6edf3chunkedTb4b4b4(Te6edf3MAX_BIND_PARAMSTb4b4b4)
Tb4b4b4.Te6edf3flatMap Tb4b4b4{ Te6edf3getNodeEntitiesByNumsTb4b4b4(Tffa657itTb4b4b4) Tb4b4b4}
Tb4b4b4.Te6edf3associateBy Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3num Tb4b4b4}

T8b949e// Partition into updates vs. inserts and resolve existing nodes in-memory
Tff7b72val Te6edf3result Tff7b72= Te6edf3mutableListOfTff7b72<Te6edf3NodeEntityTff7b72>Tb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3newNodes Tff7b72= Te6edf3mutableListOfTff7b72<Te6edf3NodeEntityTff7b72>Tb4b4b4(Tb4b4b4)
Tff7b72for Tb4b4b4(Te6edf3incoming Tff7b72in Te6edf3incomingNodesTb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3existing Tff7b72= Te6edf3existingNodesMapTff7b72[Te6edf3incomingTb4b4b4.Te6edf3numTff7b72]
Tff7b72if Tb4b4b4(Te6edf3existing Tff7b72!Tff7b72= Tff7b72nullTb4b4b4) Tb4b4b4{
Te6edf3resultTb4b4b4.Te6edf3addTb4b4b4(Te6edf3handleExistingNodeUpsertValidationTb4b4b4(Te6edf3existingTb4b4b4, Te6edf3incomingTb4b4b4, Te6edf3incomingTb4b4b4.Te6edf3num Tff7b72=Tff7b72= Te6edf3selfNumTb4b4b4)Tb4b4b4)
Tb4b4b4} Tff7b72else Tb4b4b4{
Te6edf3newNodesTb4b4b4.Te6edf3addTb4b4b4(Te6edf3incomingTb4b4b4)
Tb4b4b4}
Tb4b4b4}

T8b949e// Batch validate new nodes' public keys (one query instead of N)
Tff7b72val Te6edf3publicKeysToCheck Tff7b72= Te6edf3newNodesTb4b4b4.Te6edf3mapNotNull Tb4b4b4{ Te6edf3node Tff7b72-Tff7b72> Te6edf3nodeTb4b4b4.Te6edf3publicKeyTff7b72?.Te6edf3takeIf Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3size Tff7b72> T79c0ff0 Tb4b4b4} Tb4b4b4}Tb4b4b4.Te6edf3distinctTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3pkConflicts Tff7b72=
Tff7b72if Tb4b4b4(Te6edf3publicKeysToCheckTb4b4b4.Te6edf3isNotEmptyTb4b4b4(Tb4b4b4)Tb4b4b4) Tb4b4b4{
Te6edf3publicKeysToCheck
Tb4b4b4.Te6edf3chunkedTb4b4b4(Te6edf3MAX_BIND_PARAMSTb4b4b4)
Tb4b4b4.Te6edf3flatMap Tb4b4b4{ Te6edf3findNodesByPublicKeysTb4b4b4(Tffa657itTb4b4b4) Tb4b4b4}
Tb4b4b4.Te6edf3associateBy Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3publicKey Tb4b4b4}
Tb4b4b4} Tff7b72else Tb4b4b4{
Te6edf3emptyMapTb4b4b4(Tb4b4b4)
Tb4b4b4}

Tff7b72for Tb4b4b4(Te6edf3newNode Tff7b72in Te6edf3newNodesTb4b4b4) Tb4b4b4{
Tff7b72if Tb4b4b4(Tb4b4b4(Te6edf3newNodeTb4b4b4.Te6edf3publicKeyTff7b72?.Te6edf3size Tff7b72?: T79c0ff0Tb4b4b4) Tff7b72> T79c0ff0Tb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3conflicting Tff7b72= Te6edf3pkConflictsTff7b72[Te6edf3newNodeTb4b4b4.Te6edf3publicKeyTff7b72]
Tff7b72if Tb4b4b4(Te6edf3conflicting Tff7b72!Tff7b72= Tff7b72null Tff7b72&Tff7b72& Te6edf3conflictingTb4b4b4.Te6edf3num Tff7b72!Tff7b72= Te6edf3newNodeTb4b4b4.Te6edf3numTb4b4b4) Tb4b4b4{
T8b949e// Same key under a different num. Migrate when this is the connected device itself
T8b949e// (device-authoritative over the local link) or the num is the canonical crc32(key)
T8b949e// number (firmware 2.8 renumber); otherwise keep the existing identity.
Tff7b72if Tb4b4b4(Te6edf3newNodeTb4b4b4.Te6edf3num Tff7b72=Tff7b72= Te6edf3selfNum Tff7b72|Tff7b72| Te6edf3newNodeTb4b4b4.Te6edf3hasCanonicalNumTb4b4b4(Tb4b4b4)Tb4b4b4) Tb4b4b4{
Te6edf3migrateNodeIdentityTb4b4b4(Te6edf3from Tff7b72= Te6edf3conflictingTb4b4b4, Te6edf3to Tff7b72= Te6edf3newNodeTb4b4b4)
Te6edf3removedNums Tff7b72+Tff7b72= Te6edf3conflictingTb4b4b4.Te6edf3num
Te6edf3resultTb4b4b4.Te6edf3addTb4b4b4(Te6edf3newNodeTb4b4b4)
Tb4b4b4} Tff7b72else Tb4b4b4{
Te6edf3resultTb4b4b4.Te6edf3addTb4b4b4(Te6edf3conflictingTb4b4b4)
Tb4b4b4}
Tb4b4b4} Tff7b72else Tb4b4b4{
Te6edf3resultTb4b4b4.Te6edf3addTb4b4b4(Te6edf3newNodeTb4b4b4)
Tb4b4b4}
Tb4b4b4} Tff7b72else Tb4b4b4{
Te6edf3resultTb4b4b4.Te6edf3addTb4b4b4(Te6edf3newNodeTb4b4b4)
Tb4b4b4}
Tb4b4b4}

T8b949e// Drop batch entries for identities that were migrated away — a device mid-transition can
T8b949e// still list the old num alongside the new one, and re-inserting it would resurrect the ghost.
Te6edf3resultTb4b4b4.Te6edf3removeAll Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3num Tff7b72in Te6edf3removedNums Tb4b4b4}
Tff7b72return Te6edf3result
Tb4b4b4}

Tf0883e@QueryTb4b4b4(Ta5d6ff"Ta5d6ffSELECT * FROM my_node LIMIT 1Ta5d6ff"Tb4b4b4)
Tff7b72suspend Tff7b72fun Td2a8ffgetMyNodeEntityTb4b4b4(Tb4b4b4)Tb4b4b4: Te6edf3MyNodeEntity?

T8b949e/** Re-scopes stored messages from one owning node number to another (see `Packet.myNodeNum`). */
Tf0883e@QueryTb4b4b4(Ta5d6ff"Ta5d6ffUPDATE packet SET myNodeNum = :newNum WHERE myNodeNum = :oldNumTa5d6ff"Tb4b4b4)
Tff7b72suspend Tff7b72fun Td2a8ffremapPacketOwnerTb4b4b4(Te6edf3oldNumTb4b4b4: Tffa657IntTb4b4b4, Te6edf3newNumTb4b4b4: Tffa657IntTb4b4b4)

T8b949e/** OR IGNORE: myNodeNum is part of the reactions PK; drop the rare duplicate instead of aborting. */
Tf0883e@QueryTb4b4b4(Ta5d6ff"Ta5d6ffUPDATE OR IGNORE reactions SET myNodeNum = :newNum WHERE myNodeNum = :oldNumTa5d6ff"Tb4b4b4)
Tff7b72suspend Tff7b72fun Td2a8ffremapReactionOwnerTb4b4b4(Te6edf3oldNumTb4b4b4: Tffa657IntTb4b4b4, Te6edf3newNumTb4b4b4: Tffa657IntTb4b4b4)

Tf0883e@QueryTb4b4b4(Ta5d6ff"Ta5d6ffDELETE FROM reactions WHERE myNodeNum = :oldNumTa5d6ff"Tb4b4b4)
Tff7b72suspend Tff7b72fun Td2a8ffdeleteReactionsForOwnerTb4b4b4(Te6edf3oldNumTb4b4b4: Tffa657IntTb4b4b4)

T8b949e/** Contact keys are `"$channel$userId"`, so a suffix match rewrites just the DM threads of one node. */
Tf0883e@QueryTb4b4b4(Ta5d6ff"Ta5d6ffUPDATE packet SET contact_key = replace(contact_key, :oldId, :newId) WHERE contact_key LIKE '%' || :oldIdTa5d6ff"Tb4b4b4)
Tff7b72suspend Tff7b72fun Td2a8ffremapDmContactKeyTb4b4b4(Te6edf3oldIdTb4b4b4: Tffa657StringTb4b4b4, Te6edf3newIdTb4b4b4: Tffa657StringTb4b4b4)

Tf0883e@QueryTb4b4b4(
Ta5d6ff"Ta5d6ffUPDATE OR IGNORE contact_settings SET contact_key = replace(contact_key, :oldId, :newId) Ta5d6ff" Tff7b72+
Ta5d6ff"Ta5d6ffWHERE contact_key LIKE '%' || :oldIdTa5d6ff"Tb4b4b4,
Tb4b4b4)
Tff7b72suspend Tff7b72fun Td2a8ffremapDmContactSettingsTb4b4b4(Te6edf3oldIdTb4b4b4: Tffa657StringTb4b4b4, Te6edf3newIdTb4b4b4: Tffa657StringTb4b4b4)

Tf0883e@QueryTb4b4b4(Ta5d6ff"Ta5d6ffDELETE FROM contact_settings WHERE contact_key LIKE '%' || :oldIdTa5d6ff"Tb4b4b4)
Tff7b72suspend Tff7b72fun Td2a8ffdeleteDmContactSettingsTb4b4b4(Te6edf3oldIdTb4b4b4: Tffa657StringTb4b4b4)

T8b949e/**
* Reconciles a change of the connected device's node number before a config install replaces `my_node`.
*
* This database was selected by transport address, so a new node number arriving at the same address is, by
* default, the same physical device under a new identity: a firmware 2.8 upgrade (`my_node_num` became
* `crc32(public_key)` instead of the MAC-derived number), an erase-and-reflash, a manual key regeneration or import
* — all of which renumber the device. Message history and app-local node state follow the device.
*
* The public key is deliberately NOT used to establish continuity here: keys can be regenerated or imported by the
* user, and under firmware 2.8 every key change is also a renumber. The only veto is a hardware one — when both
* sessions reported a factory-burned device id ([MyNodeEntity.deviceId]) and they differ, this is provably
* different hardware (e.g. a reused TCP address), so the old history stays scoped under the old number instead of
* leaking to the new device.
*/
Tff7b72private Tff7b72suspend Tff7b72fun Td2a8ffmigrateLocalNodeIdentityTb4b4b4(Te6edf3previousTb4b4b4: Te6edf3MyNodeEntityTb4b4b4, Te6edf3miTb4b4b4: Te6edf3MyNodeEntityTb4b4b4, Te6edf3incomingSelfTb4b4b4: Te6edf3NodeEntity?Tb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3oldNum Tff7b72= Te6edf3previousTb4b4b4.Te6edf3myNodeNum
Tff7b72val Te6edf3newNum Tff7b72= Te6edf3miTb4b4b4.Te6edf3myNodeNum
Tff7b72val Te6edf3oldDeviceId Tff7b72= Te6edf3validDeviceIdOrNullTb4b4b4(Te6edf3previousTb4b4b4.Te6edf3deviceIdTb4b4b4)
Tff7b72val Te6edf3newDeviceId Tff7b72= Te6edf3validDeviceIdOrNullTb4b4b4(Te6edf3miTb4b4b4.Te6edf3deviceIdTb4b4b4)
Tff7b72val Te6edf3differentHardware Tff7b72= Te6edf3oldDeviceId Tff7b72!Tff7b72= Tff7b72null Tff7b72&Tff7b72& Te6edf3newDeviceId Tff7b72!Tff7b72= Tff7b72null Tff7b72&Tff7b72& Te6edf3oldDeviceId Tff7b72!Tff7b72= Te6edf3newDeviceId

Tff7b72val Te6edf3oldSelf Tff7b72= Te6edf3getNodeByNumTb4b4b4(Te6edf3oldNumTb4b4b4)Tff7b72?.Te6edf3node
Tff7b72if Tb4b4b4(Tff7b72!Te6edf3differentHardwareTb4b4b4) Tb4b4b4{
Te6edf3remapPacketOwnerTb4b4b4(Te6edf3oldNumTb4b4b4, Te6edf3newNumTb4b4b4)
Te6edf3remapReactionOwnerTb4b4b4(Te6edf3oldNumTb4b4b4, Te6edf3newNumTb4b4b4)
Te6edf3deleteReactionsForOwnerTb4b4b4(Te6edf3oldNumTb4b4b4)
Tb4b4b4}
Tff7b72if Tb4b4b4(Tff7b72!Te6edf3differentHardware Tff7b72&Tff7b72& Te6edf3oldSelf Tff7b72!Tff7b72= Tff7b72null Tff7b72&Tff7b72& Te6edf3incomingSelf Tff7b72!Tff7b72= Tff7b72nullTb4b4b4) Tb4b4b4{
Te6edf3migrateNodeIdentityTb4b4b4(Te6edf3from Tff7b72= Te6edf3oldSelfTb4b4b4, Te6edf3to Tff7b72= Te6edf3incomingSelfTb4b4b4)
Tb4b4b4} Tff7b72else Tb4b4b4{
T8b949e// Nothing to carry over — just drop the stale self row so it can't shadow the new identity.
Te6edf3deleteNodeTb4b4b4(Te6edf3oldNumTb4b4b4)
Te6edf3deleteMetadataTb4b4b4(Te6edf3oldNumTb4b4b4)
Tb4b4b4}
Tb4b4b4}

T8b949e/**
* Installs the config download from the connected device, reconciling any node-number change of the device itself
* (firmware 2.8 renumber, erase-and-reflash, manual re-key) before the new `my_node` row lands.
*
* @return node numbers whose rows were removed by identity migration, so callers can evict them from in-memory
* caches.
*/
Tf0883e@Transaction
Tff7b72suspend Tff7b72fun Td2a8ffinstallConfigTb4b4b4(Te6edf3miTb4b4b4: Te6edf3MyNodeEntityTb4b4b4, Te6edf3nodesTb4b4b4: Te6edf3ListTff7b72<Te6edf3NodeEntityTff7b72>Tb4b4b4)Tb4b4b4: Te6edf3ListTff7b72<Tffa657IntTff7b72> Tb4b4b4{
Tff7b72val Te6edf3removedNums Tff7b72= Te6edf3mutableListOfTff7b72<Tffa657IntTff7b72>Tb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3previous Tff7b72= Te6edf3getMyNodeEntityTb4b4b4(Tb4b4b4)
Tff7b72if Tb4b4b4(Te6edf3previous Tff7b72!Tff7b72= Tff7b72null Tff7b72&Tff7b72& Te6edf3previousTb4b4b4.Te6edf3myNodeNum Tff7b72!Tff7b72= Te6edf3miTb4b4b4.Te6edf3myNodeNumTb4b4b4) Tb4b4b4{
Te6edf3migrateLocalNodeIdentityTb4b4b4(Te6edf3previousTb4b4b4, Te6edf3miTb4b4b4, Te6edf3nodesTb4b4b4.Te6edf3find Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3num Tff7b72=Tff7b72= Te6edf3miTb4b4b4.Te6edf3myNodeNum Tb4b4b4}Tb4b4b4)
Te6edf3removedNums Tff7b72+Tff7b72= Te6edf3previousTb4b4b4.Te6edf3myNodeNum
Tb4b4b4}
Te6edf3clearMyNodeInfoTb4b4b4(Tb4b4b4)
Te6edf3setMyNodeInfoTb4b4b4(Te6edf3miTb4b4b4)
Te6edf3putAllTb4b4b4(Te6edf3getVerifiedNodesForUpsertTb4b4b4(Te6edf3nodesTb4b4b4, Te6edf3selfNum Tff7b72= Te6edf3miTb4b4b4.Te6edf3myNodeNumTb4b4b4, Te6edf3removedNums Tff7b72= Te6edf3removedNumsTb4b4b4)Tb4b4b4)
Tff7b72return Te6edf3removedNums
Tb4b4b4}

T8b949e/**
* Backfills longName and shortName columns from the user protobuf for nodes where these columns are NULL. This
* ensures search functionality works for all nodes. Skips placeholder/default users (hwModel == UNSET).
*/
Tf0883e@Transaction
Tff7b72suspend Tff7b72fun Td2a8ffbackfillDenormalizedNamesTb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3nodes Tff7b72= Te6edf3getAllNodesSnapshotTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3nodesToUpdate Tff7b72=
Te6edf3nodes
Tb4b4b4.Te6edf3filter Tb4b4b4{ Te6edf3node Tff7b72-Tff7b72>
T8b949e// Only backfill if columns are NULL AND the user is not a placeholder (hwModel != UNSET)
Tb4b4b4(Te6edf3nodeTb4b4b4.Te6edf3longName Tff7b72=Tff7b72= Tff7b72null Tff7b72|Tff7b72| Te6edf3nodeTb4b4b4.Te6edf3shortName Tff7b72=Tff7b72= Tff7b72nullTb4b4b4) Tff7b72&Tff7b72& Te6edf3nodeTb4b4b4.Te6edf3userTb4b4b4.Te6edf3hw_model Tff7b72!Tff7b72= Te6edf3HardwareModelTb4b4b4.Te6edf3UNSET
Tb4b4b4}
Tb4b4b4.Te6edf3map Tb4b4b4{ Te6edf3node Tff7b72-Tff7b72> Te6edf3nodeTb4b4b4.Te6edf3copyTb4b4b4(Te6edf3longName Tff7b72= Te6edf3nodeTb4b4b4.Te6edf3userTb4b4b4.Te6edf3long_nameTb4b4b4, Te6edf3shortName Tff7b72= Te6edf3nodeTb4b4b4.Te6edf3userTb4b4b4.Te6edf3short_nameTb4b4b4) Tb4b4b4}
Tff7b72if Tb4b4b4(Te6edf3nodesToUpdateTb4b4b4.Te6edf3isNotEmptyTb4b4b4(Tb4b4b4)Tb4b4b4) Tb4b4b4{
Te6edf3putAllTb4b4b4(Te6edf3nodesToUpdateTb4b4b4)
Tb4b4b4}
Tb4b4b4}

Tf0883e@QueryTb4b4b4(Ta5d6ff"Ta5d6ffSELECT * FROM nodesTa5d6ff"Tb4b4b4)
Tff7b72suspend Tff7b72fun Td2a8ffgetAllNodesSnapshotTb4b4b4(Tb4b4b4)Tb4b4b4: Te6edf3ListTff7b72<Te6edf3NodeEntityTff7b72>
Tb4b4b4}

Served by rngit 1.5.0 - Generated in 0.11s